/*
 ----------------------------------------------------------------------------------
                           "THE BEER-WARE COPYRIGHT LICENSE"
                                     (Revision 43)
                                      -----------
    Rymek [rymek@rymek.org] wrote this file. As long as You retain this notice and
    this file has been sent directly to You by the author, You can do whatever You
    want with this stuff as long as You don't sell or rent it. If we meet some day,
    and You think this stuff is worth it, You can buy me a beer in return :-)
                                                                 __________
                                                                 Paweł Rymaszewski
  ----------------------------------------------------------------------------------
 */

package utils.tns;

/*
 * TNSParser.java
 *
 * Author: Rauf Sarwar <br>
 * Usage: Freeware. Modify/Distribute as you wish. <br>
 *
 * Modified by Bertalan Lacza
 * Modified by Rymek (Paweł Rymaszewski)
 */


import exceptions.NoTnsAdminFileException;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Comparator;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Parse Oracle's Tnsnames.ora file
 */
public class TnsParser {

    private File tnsnamesora = null;                         // Tnsnames.ora File object
    private String filestr = "";                             // Holds Tnsnames.ora file contents
    private String aliasstr = "";                            // Holds delimited Alias/Service name information
    private String eol = "";                                 // Standard End-Of-Line Carriage Return\Line feed
    private final static String SPACE_REP = " ";             // Space identifier
    private final static String NULL_REP = "";               // null identifier
    private final static char COMMCHAR = '#';                // Comment identifier
    private final static String STR_CHAR_5 = (char) 5 + "";  // (char)5 identifier

    // Constants
    private static final String HOST = "HOST";
    private static final String PORT = "PORT";
    private static final String SID = "SID";
    private static final String ADDRESS = "ADDRESS";
    private static final String CONNECT_DATA = "CONNECT_DATA";
    private static final String SERVICE_NAME = "SERVICE_NAME";
    private static final String DESCRIPTION = "DESCRIPTION";

    private static TnsParser instance = null;

    /**
     * Constructs TNSParse object used to parse Tnsnames.ora file
     *
     * @param tnsFile: File object
     * @throws NoTnsAdminFileException when tns parsing fails
     * @throws IOException             when file read error occurs
     */
    TnsParser(File tnsFile)
            throws NoTnsAdminFileException, IOException {
        this.tnsnamesora = tnsFile;
        this.eol = getEndOfLine();
        tnsReadAndParseFile();
    }

    @SuppressWarnings({"UnusedAssignment"})
    public static TnsParser getInstance(String location)
            throws Exception {
        File tnsFile = new File(location + File.separator + "tnsnames.ora");
        if (!tnsFile.exists()) {
            throw new NoTnsAdminFileException();
        }
        /* Create tnsparser instance if it changed since last time */
        long lastSize = 0;
        long lastModified = 0;
        String lastLocation = "";
        if (instance == null || !lastLocation.equals(location)
                || lastSize != tnsFile.length()
                || lastModified != tnsFile.lastModified()) {
            instance = new TnsParser(tnsFile);
            lastSize = tnsFile.length();
            lastModified = tnsFile.lastModified();
            lastLocation = location;
        }
        return instance;
    }

    /* PUBLIC METHODS */

    /**
     * Return clean and continuous (char)5 separated list of all connect strings in the file.
     *
     * @return (char)5 separated list of all connect strings in the file.
     */
    public String getStringList() {
        return filestr;
    }

    /**
     * Return End-Of-Line delimited list of Alias/Service names.
     *
     * @return End-Of-Line delimited list of Alias/Service names
     */
    String getAliasList() {
        return aliasstr;
    }

    /**
     * Return full connect string. Full connect string begins with Alias/Service name and ends with
     * last ")".
     *
     * @param alias Alias/Service name
     * @return Full connect string
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    public String getFullConnString(String alias)
            throws NoTnsAdminFileException {
        checkAliasExist(alias);
        return getConnectString(alias);
    }

    /**
     * Extracts a TCP string if available, builds the string as used by Oracle thin jdbc driver.
     *
     * @param alias Alias/Service name
     * @return Fully concatenated Thin TCP string like <HOST>: <PORT>: <SID>
     *         OR null if does not exist
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    public String extractTcpString(String alias)
            throws NoTnsAdminFileException {
        checkAliasExist(alias);
        String connstr = getConnectString(alias);
        if (isTcpAvailable(connstr))
            return null;
        return buildThinString(getFirstTcpString(connstr));
    }

    /**
     * Returns the host for this alias.
     *
     * @param alias Alias/Service name
     * @return The host
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    public String getHost(String alias)
            throws NoTnsAdminFileException {
        checkAliasExist(alias);
        String connstr = getConnectString(alias);
        if (isTcpAvailable(connstr))
            return null;
        return parseTcpString(getFirstTcpString(connstr),
                              HOST);
    }

    /**
     * Returns the port number for this alias.
     *
     * @param alias Alias/Service name
     * @return The port number
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    public String getPort(String alias)
            throws NoTnsAdminFileException {
        checkAliasExist(alias);
        String connstr = getConnectString(alias);
        if (isTcpAvailable(connstr))
            return null;
        return parseTcpString(getFirstTcpString(connstr),
                              PORT);
    }

    /**
     * Returns the connection data.
     *
     * @param alias Alias/Service name
     * @return The connection data
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    public String getConnectData(String alias)
            throws NoTnsAdminFileException {
        checkAliasExist(alias);
        String connstr = getConnectString(alias);
        if (isTcpAvailable(connstr))
            return null;
        String conn = parseTcpString(getFirstTcpString(connstr),
                                     SERVICE_NAME);
        if (conn.equals(""))
            conn = parseTcpString(getFirstTcpString(connstr),
                                  SID);
        if (conn.equals(""))
            throw new NoTnsAdminFileException("Either SERVICE_NAME nor SID defined in tnsnames.ora.");
        return conn;
    }

    /**
     * Returns all available databases.
     *
     * @return All available databases
     */
    @SuppressWarnings("unchecked")
    public String[] getDatabaseNames() {
        SortedMap<String, String> map = new TreeMap<String, String>(new Comparator() {
            /* (non-Javadoc)
            * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
            */
            public int compare(Object arg0,
                               Object arg1) {
                return ((String) arg0).compareTo((String) arg1);
            }
        });
        String[] list = getAliasList().split(TnsParser.getEndOfLine());
        for (String aList : list)
            map.put(aList,
                    "");

        int i = 0;
        for (String s : map.keySet()) list[i++] = s;

        return list;
    }

    /**
     * Parses a connection string and returns all defined ADDRESSES.
     *
     * @param alias Alias/Service name
     * @return End-Of-Line delimited list of all addresses including CONNECT_DATA
     *         (ADDRESS=(.....))(CONNECT_DATA=(...))
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    public String enumerateAddresses(String alias)
            throws NoTnsAdminFileException {
        checkAliasExist(alias);
        String connstr = getConnectString(alias);
        return enumerateAddressList(connstr);
    }

    /**
     * Only checks if all parenthesis "(" and ")" are matched
     *
     * @param alias: Alias/Service name
     * @return boolean: true if valid, false otherwise
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    public boolean isConnStrValid(String alias)
            throws NoTnsAdminFileException {
        checkAliasExist(alias);
        String strtmp = getConnectString(alias);
        strtmp = strtmp + " ";
        int i = 0;
        int j = 0;
        int countera = 0;
        int counterb = 0;
        while (true) {
            i = strtmp.indexOf("(",
                               i + 1);
            if (i == -1)
                break;
            countera = countera + 1;
        }
        while (true) {
            j = strtmp.indexOf(")",
                               j + 1);
            if (j == -1)
                break;
            counterb = counterb + 1;
        }
        return countera == counterb;
    }

    /* PRIVATE METHODS */

    /**
     * Return full connection string from &lt;ServiceName&gt; to to last ")"
     *
     * @param alias: Alias/Service name
     * @return String: Full connection string
     */
    private String getConnectString(String alias) {
        String retstr;
        retstr = "";
        String tokstr;
        String strtmp = alias.toUpperCase().trim();
        StringTokenizer st = new StringTokenizer(filestr,
                                                 STR_CHAR_5);
        while (st.hasMoreTokens()) {
            tokstr = st.nextToken().trim();
            if (tokstr.startsWith(strtmp)) {
                if ((tokstr.charAt(strtmp.length()) == '.')
                        || (tokstr.charAt(strtmp.length()) == '=')) {
                    retstr = tokstr;
                    break;
                }
            }
        }
        return retstr;
    }

    /**
     * Checks if Alias/Service name exists
     *
     * @param alias: Alias/Service name
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    private void checkAliasExist(String alias)
            throws NoTnsAdminFileException {
        String strtmp = getConnectString(alias);
        if (strtmp.equalsIgnoreCase(""))
            throw new NoTnsAdminFileException("Service name '" + alias.toUpperCase()
                                                      + "' does not exist in file '" + tnsnamesora.getAbsolutePath()
                                                      + "'");
    }

    /**
     * Checks if connection string has TCP protocol defined
     *
     * @param connstr: Full connection string to test
     * @return boolean: true if TCP available, false otherwise
     */
    private boolean isTcpAvailable(String connstr) {
        return !connstr.contains("(PROTOCOL=TCP)");
    }

    /**
     * Returns first available TCP connection string used by getThinTcpConn()
     *
     * @param connstr: Full connection string to parse
     * @return String: First available TCP connection string
     */
    private String getFirstTcpString(String connstr) {
        String retstr = "";
        int i;
        int j = 0;

        while (true) {
            i = connstr.indexOf("(" + ADDRESS + "=",
                                j);
            if (i == -1)
                break;
            i = connstr.indexOf("=",
                                i) + 1;
            j = connstr.indexOf("))",
                                i) + 1;
            if ((i != -1) && (j != -1))
                retstr = connstr.substring(i,
                                           j);
            if (retstr.contains("PROTOCOL=TCP")) {
                i = connstr.indexOf("(" + CONNECT_DATA + "=",
                                    j);
                j = connstr.length();
                if ((i != -1) && (j != -1))
                    retstr = retstr + connstr.substring(i,
                                                        j);
                break;
            }
            retstr = "";
        }
        return retstr;
    }

    /**
     * Parses TCP string and returns value of "HOST", "PORT" etc
     *
     * @param tcpstring: Unparsed TCP string returned by getFirstTcpString()
     * @param getval:    Value to get e.g. "HOST", "PORT" etc
     * @return - value of "HOST", "PORT" etc
     */
    private String parseTcpString(String tcpstring,
                                  String getval) {
        String retstr = "";
        int i;
        int j;
        if (getval.equalsIgnoreCase(CONNECT_DATA)) {
            i = tcpstring.indexOf(getval);
            i = tcpstring.indexOf("=",
                                  i);
            i = tcpstring.indexOf("(",
                                  i);
            i = tcpstring.indexOf("=",
                                  i) + 1;
            j = tcpstring.indexOf(")",
                                  i);
            if ((i != -1) && (j != -1))
                retstr = tcpstring.substring(i,
                                             j);
        } else {
            i = tcpstring.indexOf(getval);
            if (i == -1)
                return "";
            i = tcpstring.indexOf("=",
                                  i) + 1;
            j = tcpstring.indexOf(")",
                                  i);
            if ((i != -1) && (j != -1))
                retstr = tcpstring.substring(i,
                                             j);
        }
        return retstr;
    }

    /**
     * Parses TCP string and returns a full concatenated string e.g. &lt;HOST&gt;: &lt;PORT&gt;:
     * &lt;SID&gt;
     *
     * @param tcpstring: Unparsed TCP string returned by getFirstTcpString()
     * @return TNS string
     */
    private String buildThinString(String tcpstring) {
        String retstr = parseTcpString(tcpstring,
                                       HOST) + ":";
        retstr += parseTcpString(tcpstring,
                                 PORT) + ":";
        retstr += parseTcpString(tcpstring,
                                 CONNECT_DATA);
        return retstr;
    }

    /*
    * @param connstr: Full connection string to parse @return String: End-Of-Line delimited list of
    * all addresses including CONNECT_DATA (ADDRESS=(.....))(CONNECT_DATA=(...)) Parses a
    * connection string and returns all defined ADDRESSES
    */
    @SuppressWarnings({"UnusedAssignment"})
    private String enumerateAddressList(String connstr) {

        String retstr;
        retstr = "";
        String strtmp;
        int i;
        int j = 0;
        int k;
        int l;
        while (true) {
            i = connstr.indexOf("(" + ADDRESS + "=",
                                j);
            if (i == -1)
                break;
            j = connstr.indexOf("))",
                                i);
            j = connstr.indexOf("(",
                                j);
            if ((j != -1)) {
                strtmp = connstr.substring(i,
                                           j);
                if (strtmp.contains("(PROTOCOL=BEQ)"))
                    strtmp = strtmp.substring(0,
                                              strtmp.indexOf("'))") + 3);
                else
                    strtmp = strtmp.substring(0,
                                              strtmp.indexOf("))") + 2);
                retstr = retstr + strtmp;
                strtmp = "";
            }
            k = connstr.indexOf("(" + CONNECT_DATA + "=",
                                j);
            l = connstr.indexOf("))",
                                k) + 2;
            if ((k != -1) && (l != -1))
                retstr = retstr + connstr.substring(k,
                                                    l) + eol;
        }
        if (retstr.endsWith(eol))
            retstr = retstr.substring(0,
                                      retstr.length() - 1);
        return retstr;
    }

    /**
     * @return String: End Of Line according to platform Get End Of Line according to
     *         platform
     */
    private static String getEndOfLine() {
        return File.separator.equals("/") ? "\n" : "\r\n";
    }

    /*
    * === EXCLUSIVE FILE READERS AND PARSERS
    * === These private methods exclusively Check, Read and Parse the Tnsnames.ora file
    * and build the filestr (Continuous string of services names without any spaces, CRLF,
    * comments or any Non-Service name data). Also builds aliasstr (End-Of-Line separated
    * string that contains all service names. <ServiceName>. <DomainName>.
    */

    /**
     * Read and parse Tnsnames.ora file.
     *
     * @throws NoTnsAdminFileException when TNS parse error occurs
     * @throws IOException             when file read error occurs
     */
    private void tnsReadAndParseFile()
            throws NoTnsAdminFileException, IOException {
        /* Check Tnsnames.ora file existense and readability */
        tnsCheckFileValid();
        /* Read Tnsnames.ora file using FileReader. Check if file is valid format */
        tnsReadFile();
        /* Remove all spaces and replace with null's "" */
        tnsRemoveAndReplace(SPACE_REP,
                            NULL_REP);
        /* Remove all comment lines */
        tnsRemoveComments();
        /* Remove all multiple eol's with single eol */
        tnsRemoveAndReplace(eol,
                            eol);
        /* Fix =(DESCRIPTION keyword */
        tnsFixDescKeyword();
        /* Separate all Connection strings by strchar5 */
        tnsSeparateConnStrings();
        /* Now remove all eol's with null's "". This will create one continuous string */
        tnsRemoveAndReplace(eol,
                            NULL_REP);
        /* Remove any lines that are not part of connection strings */
        /* tnsRemoveNonDataLines (); */
        /* Correctly separate multiple aliases on one line */
        tnsCorrectMultipleAliases();
        /* Set Alias/Service name string */
        tnsSetAliasString();
    }

    /*
    * private void tnsCheckFileValid () @param @return Check Tnsnames.ora file existense and
    * readability
    */
    private void tnsCheckFileValid()
            throws NoTnsAdminFileException {
        if (!tnsnamesora.isFile())
            throw new NoTnsAdminFileException("File '" + tnsnamesora.getAbsolutePath()
                                                      + "' does not exist or is not a normal file\nPlease select correct TNSNAMES.ORA file to continue");
        if (!tnsnamesora.canRead())
            throw new NoTnsAdminFileException("File '" + tnsnamesora.getAbsolutePath()
                                                      + "' is not readable\nPlease select correct TNSNAMES.ORA file to continue");
        if (tnsnamesora.length() == 0)
            throw new NoTnsAdminFileException("File '" + tnsnamesora.getAbsolutePath()
                                                      + "' is empty\nPlease select correct TNSNAMES.ORA file to continue");
    }

    /**
     * Read Tnsnames.ora file using FileReader.
     * Check if file is of valid Tnsnames.ora format.
     *
     * @throws IOException             when can't read the file
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    private void tnsReadFile()
            throws NoTnsAdminFileException, IOException {

        FileReader fr = new FileReader(tnsnamesora);
        long fsize = tnsnamesora.length();
        char[] cbuf = new char[(int) fsize];
        int offset = 0;
        int len = (int) fsize;
        fr.read(cbuf,
                offset,
                len);
        fr.close();
        String strtmp = new String(cbuf);
        filestr = "";
        /* Convert everything to UPPERCASE */
        filestr = strtmp.toUpperCase();
        /* Check file format */
        tnsCheckFileFormat();
    }

    /**
     * Remove and replace strings from filestr.
     *
     * @param remstr: String to be removed
     * @param repstr: String to be replaced with
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    private void tnsRemoveAndReplace(String remstr,
                                     String repstr)
            throws NoTnsAdminFileException {
        String strtmp = filestr;
        filestr = "";
        if (strtmp.contains(remstr)) {
            StringTokenizer st = new StringTokenizer(strtmp,
                                                     remstr);
            while (st.hasMoreTokens()) {
                filestr = filestr + st.nextToken() + repstr;
            }
        }
        if (filestr.equalsIgnoreCase(""))
            filestr = strtmp;
        tnsCheckFileFormat();
    }

    /**
     * Remove all comment lines from filestr marked by "#".
     *
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    private void tnsRemoveComments()
            throws NoTnsAdminFileException {
        String strtmp = filestr;
        filestr = "";
        if (strtmp.contains(COMMCHAR + "")) {
            StringTokenizer st = new StringTokenizer(strtmp,
                                                     eol);
            while (st.hasMoreTokens()) {
                String tokenstr = st.nextToken().trim();
                if (tokenstr.charAt(0) != COMMCHAR)
                    filestr = filestr + tokenstr + eol;
            }
        }
        if (filestr.equalsIgnoreCase(""))
            filestr = strtmp;
        tnsCheckFileFormat();
    }

    /**
     * Fix =(DESCRIPTION keyword so that there are
     * no spaces and eol's between '=' and '(DESCRIPTION'
     *
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    private void tnsFixDescKeyword()
            throws NoTnsAdminFileException {
        String strtmp = filestr;
        filestr = "";
        int i = 0;
        int j;
        if (strtmp.contains("=" + eol + "(" + DESCRIPTION)) {
            while (strtmp.indexOf("=" + eol + "(" + DESCRIPTION,
                                  i) != -1) {
                j = strtmp.indexOf("=" + eol + "(" + DESCRIPTION,
                                   i);
                filestr = filestr + strtmp.substring(i,
                                                     j + 1);
                i = strtmp.indexOf("(",
                                   j);
                if (strtmp.indexOf("=" + eol + "(" + DESCRIPTION,
                                   i) == -1)
                    filestr = filestr + strtmp.substring(i,
                                                         strtmp.length());
            }
        }
        if (filestr.equalsIgnoreCase(""))
            filestr = strtmp;
        tnsCheckFileFormat();
    }

    /**
     * Separate all Connection strings using constant strchar5.
     *
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    private void tnsSeparateConnStrings()
            throws NoTnsAdminFileException {

        String strtmp = filestr;
        filestr = "";
        boolean bExit = false;
        int i = 0;
        int j;
        if (strtmp.contains("=(" + DESCRIPTION)) {
            while (true) {
                j = strtmp.indexOf("=(" + DESCRIPTION,
                                   strtmp.indexOf("=(" + DESCRIPTION,
                                                  i) + 20);
                if (j == -1) {
                    j = strtmp.length();
                    bExit = true;
                } else {
                    j = strtmp.substring(0,
                                         j).lastIndexOf(")") + 1;
                }
                filestr = filestr + strtmp.substring(i,
                                                     j) + STR_CHAR_5;
                if (bExit)
                    break;
                i = j;
            }
        }
        if (filestr.equalsIgnoreCase(""))
            filestr = strtmp;
        tnsCheckFileFormat();
    }

    /**
     * Correctly separate multiple aliases on one line
     *
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    private void tnsCorrectMultipleAliases()
            throws NoTnsAdminFileException {
        String strtmp = filestr;
        filestr = "";
        AtomicReference<String> tokstr;
        tokstr = new AtomicReference<>();
        String aliaslist;
        StringTokenizer st;
        st = new StringTokenizer(strtmp,
                                 STR_CHAR_5);
        int i;
        int j;
        while (st.hasMoreTokens()) {
            tokstr.set(st.nextToken());
            i = tokstr.get().indexOf("=",
                                     0);
            j = tokstr.get().lastIndexOf(")") + 1;
            if (i != -1) {
                aliaslist = tokstr.get().substring(0,
                                                   i);
                StringTokenizer st2 = new StringTokenizer(aliaslist,
                                                          ",");
                while (st2.hasMoreTokens()) {
                    filestr = filestr + st2.nextToken() + tokstr.get().substring(i,
                                                                                 j)
                            + STR_CHAR_5;
                }
            }
        }
        if (filestr.equalsIgnoreCase(""))
            filestr = strtmp;
        tnsCheckFileFormat();
    }

    /**
     * Set End-Of-Line delimited aliasstr that contains all service names.
     */
    private void tnsSetAliasString() {
        aliasstr = "";
        String tokstr;
        StringTokenizer st = new StringTokenizer(filestr,
                                                 STR_CHAR_5);
        while (st.hasMoreTokens()) {
            tokstr = st.nextToken();
            if (tokstr.indexOf("=",
                               0) != -1)
                aliasstr = aliasstr + tokstr.substring(0,
                                                       tokstr.indexOf("=",
                                                                      0)) + eol;
        }
        if (aliasstr.endsWith(eol))
            aliasstr = aliasstr.substring(0,
                                          aliasstr.length() - 1);
    }

    /**
     * Check filestr for Tnsnames.ora file keywords. If missing, throw Exception.
     *
     * @throws NoTnsAdminFileException when tns parsing fails
     */
    private void tnsCheckFileFormat()
            throws NoTnsAdminFileException {

        if (filestr.length() > 0) {
            if ((!filestr.contains(DESCRIPTION))
                    || (!filestr.contains(ADDRESS))
                    || (!filestr.contains(CONNECT_DATA)))
                throw new NoTnsAdminFileException("File '"
                                                          + tnsnamesora.getAbsolutePath()
                                                          + "' is not an Oracle tnsnames.ora format file OR unable to parse.\nPlease select correct TNSNAMES.ORA file to continue");
        }
    }
}
